Alrighty, in this lecture, we're now going to move on to scripting the AI for our zombies.
We're going to be implementing the pathfinding service in order to create a zombie that can path, find,
and chase after the closest available player.
Now, before we start scripting our zombies, we want to do the same thing that we did for our guns
and create a module script that's going to store all of the properties for each of our zombies.
We want to be able to customize the health of our zombie, how fast our zombie moves when chasing a
player, whether our zombie regenerates health or not, how much damage the zombie deals, and so on.
So if we head to server storage and we go to the zombies folder, we have a bunch of different zombies
that I'm going to be planning to add into my game.
For now, let's just move the basic original zombie into our workspace, and I'll just place them in
my zombies folder.
And here's my zombie right here.
And inside of him there's not much.
It's just a humanoid with some limbs.
This is just basically a AR 15 rig with the colors changed and a different face added onto it.
But we do have our hulas damaged object value in here, which we're going to be using to keep track
of who killed a zombie.
And other than that, this is just a basic humanoid setup for our zombie.
So that means we need to script all of the different functionality for actually moving our zombie around
and chasing our players.
But as I said before we do that, let's go ahead and create a new module script inside of our zombie
that's going to be storing all of the properties for it.
Inside of here, I'll just create a table called self, and I'll make sure to return that at the end
of this module script.
And inside of here we can go ahead and define a whole bunch of different things for our AI.
One of the first things I want to go ahead and define are the different properties we want to manipulate
for the zombies humanoid, such as the health and how far the zombie can jump.
So I'm going to create a key here, and I'm going to call it humanoid stats.
And I'm going to set that equal to a table.
And inside of here we can go ahead and store a bunch of key value pairs for different properties in
the humanoid, such as the max health, the health, the jump power, things like that.
So for the max health for this zombie, I'm going to set it to 200 health points.
And then for the health or the starting health of the zombie, I'm also going to set it at 200 health
points.
And then for the jump power, I'm going to set the jump power equal to 50, which is the default.
Now some other things I want to go ahead and define are the different speeds for my zombie.
For example, I want to define a walk speed for when my zombie is just wandering around.
And I also want to walk speed for my zombies chasing after a player so we can have two speeds.
One we'll call the chase speed, and then we'll have another one called the Wander Speed.
Now for the chasing speed.
Oops.
Let me put a comma here for the chasing speed.
I want it to be a little bit slower than the player because this is just our regular zombie.
It's not like our fast zombies that are going to be able to run.
So I'll set the chasing speed to a value of like 14.
And then for the wandering speed, I'm going to set it to a low value like five.
Next up, we want to be able to add regeneration to our zombies if we so choose.
So for this particular zombie, yes, I would like the zombie to regenerate health once the health of
the zombie goes down a specific threshold.
So to define that we can create a boolean we'll call it regenerates health.
And I'm going to set that equal to true.
And then we need to define how much health we should generate and how often we should regenerate that
health.
So I'll call this my health regen amount.
And we could do like five health points.
And we want to regenerate five health points every let's say second.
So health regen tick.
We can call this it'll be one.
So we'll regenerate five health points every single second.
However I also want to create a limit for this health regeneration.
So I'll call this my health regen limit.
And I'm going to set it to a value of 0.5.
Which means once my zombie reaches at least half of its health, I'm not going to regenerate health
anymore.
So if the zombies health gets really low, let's say it gets to like ten a health points, then yes,
I would like the zombie to regenerate health until it gets half of its health back, and then once it
gets half of its health back, we're not going to regenerate the health anymore.
We also don't want to regenerate any health until the health goes below that halfway mark.
So this is going to be our limit for regenerating health.
The next thing we want to go ahead and define is the targeting distance for our zombie.
Basically, how far away can a zombie detect a player's presence.
So we'll call this our targeting distance.
And I'm going to set it to quite a high value of 150 studs.
And then I want to have another targeting distance where the zombie is more alert.
So basically when the zombies wandering around, it's going to have this 150 stud targeting distance.
Now if a player's outside of that targeting distance, but the player shoots and hurts the zombie,
well, I want the zombie to then become more alert and look a little bit farther away so we could have
another targeting distance called alert targeting distance.
And we can set this one to a little bit of a higher value, like 190 studs.
Now this next property I'm going to define here, I'm going to call it recalculate path after distance.
And I'm going to set this to a value of 20 studs.
What is this for.
Well what this is going to be for is when our zombie is pathfinding towards a particular player, we're
going to check if our zombie is able to see that player using a raycast.
If the zombie can see the player, then there's no point pathfinding.
Instead, we can have the zombie run directly towards the player.
However, if the player is pathfinding along a set of waypoints and they get 20 studs away from the
first waypoint that they path found towards, well, the zombie is going to be like, okay, I've been
pathfinding for more than 20 studs and I still haven't seen this player that I'm trying to go after.
So instead, what the zombie is going to do is it's going to stop pathfinding, it's going to recap
out the path and instead grab a new closest player.
If the new closest player is the same player we were originally pathfinding after then fine, it'll
continue to pathfind after that player, but if it finds a new player to Pathfind towards, then it's
going to Pathfind towards that other player.
So this basically just sets a value of how long we should pathfind for, until we want to recalculate
the pathfinding to see if we can find a new closest player.
Now, the next property I want to go ahead and define is how much a damage I want my zombie to deal
when it attacks a player.
So we could call this damage per hit.
And I'm going to set it to ten health points because this is just our regular zombie.
He's going to be relatively weak.
And then we want to add a cooldown or how often this zombie can attack our player.
So we'll call this cooldown between attack.
And since this is going to be the base zombie, I'm going to set it to a value of like 3.5 seconds.
So this means our zombie can deal ten damage every 3.5 seconds.
We also want to be able to define the attack range for a zombie.
So how close does our player have to be in order for our zombie to deal this damage?
And I'm going to set it to a value of three studs.
And then this next property I'm going to define, I'm going to call it the max visibility height.
And this parameter is going to tell our AI how high up a player can be before.
We don't want to pathfind after them any longer.
So for example, let's say my zombie was at the ground level.
And yet my player was like, high up in the sky somewhere on some kind of pillar or something like that.
Well, clearly my zombie is not going to see that player because they're so high up and it's not going
to be worth the time to try and pathfind towards that player.
So we want to set a threshold for the max height the zombie can pathfind towards a player.
So we're going to be comparing the position on the y axis for a zombie compared to the position on the
y axis for a player who's high up standing on something.
And I found a value of eight studs to be a good middle ground for this property, of course, you're
going to be able to tweak all of these values to your preference, but for now, I'm just going to keep
it at the value of eight.
The next thing I want to go ahead and define is how much money we should give to a player that kills
this zombie, which is why this who last damaged object value is super important because when our zombie
dies, we want to find out who last damaged the zombie and then award them some money when the zombie
dies.
So we'll call this property our money drop, and I'm going to set it equal to a table with a minimum
and a maximum value.
So we're going to randomly give the player money between these two values for the minimum.
I'm just going to set it to like $5.
And then for the max I'm going to set it to $10.
This is because it's just the base zombie.
We don't want to give players that much money for killing the base zombie.
Next, we want to go ahead and define a section for some animations.
For zombie specifically, we want to be able to have a table to store all of the different attack animation
IDs for our zombie.
So I want to put in like a couple different attack animations the zombie can have, and then we're just
going to randomly pick one of those attack animations and play it on our zombie.
So we'd have an animation there and another animation here, put the IDs in there.
And that's going to be our attack animations.
Now we don't need to define other animations in here, like the walking or the running animations.
We're going to be doing that later, so don't worry about those for now.
However, the next section inside of this module script I want to go ahead and define is the agent parameters
for the pathfinding service.
So if you remember back in the pathfinding service lecture we had, I talked about the different agent
parameters for creating a path from the pathfinding service.
So we need to define some agent parameters for the agent radius.
That's going to be equal to one stud for the zombie, which is half of the width of the humanoid route
part.
The agent height is going to be about five studs, because that is about the same value of the height
for our zombie.
The agent can jump.
We're going to set equal to true.
We're going to set.
Agent can climb equal to false because there's no trust parts in my map.
So I'm not worried about my zombie needing to climb up anything.
We're going to set the waypoint spacing equal to a value of four.
That's a good middle ground for the detailed ness of our paths.
And then we don't need to define any costs for this zombie.
But of course, if you would like to define some costs, you can totally do so I'm just going to leave
this table blank because I don't have any materials in mind that the zombie needs to avoid.
Otherwise, this last section we can go ahead and define is going to be all the different sounds for
our zombie.
So I want to randomly play different sounds.
For example, when the zombie is wandering around, we'll just play some regular groaning zombie noises.
When the zombie sees a player, then we want to play like a different sound where the zombies like,
oh shoot, I see somebody, I got to chase after them.
We want to have a section to define different death sounds.
So when a player kills a zombie, the zombie will be like, ah, and then we'll play that.
And then we also want to have some different sounds for when the zombie is attacking our player.
So when the zombie is swinging their arms, we want to play another sound for that.
So we could have a table to store different attack sound IDs.
We can have a table to store different death sound IDs.
We could have a table to define the different audio sounds for when the zombie sees a player, so we'll
call this player scene sound IDs.
And then we can also define the different growling sound IDs for when the zombie is wandering around.
So we'll call it growl sound IDs.
And inside of here is where we can go to the toolbox, find some audio that would kind of match these
different tables, and then fill their IDs into them.
Okay, so after scouring the toolbox for a little while, I found a whole bunch of sound IDs that I'm
going to be using for my attack sounds.
My death sounds when the player is seen and when the zombie is growling.
If you would like to use the exact same audio sounds that I'm going to be using, then I'll have this
chunk of text attached to the lecture so you can go ahead and copy and paste it into the properties
module script for your zombies.
Otherwise, the last thing we can go ahead and define are going to be the sound properties for these
sound IDs.
So we'll basically just have a general sound properties to apply for.
When we play the sound IDs.
For example, the volume will set to like 0.5 the roll off max distance.
We could do a distance of like 45 studs.
The roll off minimum distance we'll do like 15 studs.
And then of course the roll off mode we're going to set to the enum dot roll off mode of inverse taper.
And this should be all of the properties that we're going to need for our zombie.
Okay.
So now what we're going to do is we're going to create a new server script inside of our zombie.
And this is going to be our zombie AI script.
So we'll just call it, uh, zombie AI.
Inside of the script.
We're going to be using that same comment template that we've always been using.
We're going to get rid of that section, get rid of that section.
And one of the first things we're going to need inside of this server script is obviously the path finding
service.
So we'll create a variable for the path finding service game get service path finding service.
We're also going to need the player service.
That way we can go ahead and look through all of the different characters that are on the map and grab
the closest one to our zombie.
Now, of course, we're going to need access to the properties of our zombie, so we'll require.
We'll get our zombie, which is script dot parent, and we'll get the properties for our zombie.
And actually, let's go ahead and make a variable for our zombie.
We'll just do script dot parent and then we can pass zombie here.
Because now what we also need is we're going to need the humanoid for our zombie to be able to move
our zombie around.
So that's equal to zombie dot humanoid.
We're going to need the root part of the zombie as well, so I'll call it my root part.
That's equal to zombie dot.
Humanoid root part.
And then we're also going to need the head of our zombie, and we're going to be using the zombies heads
position to raycast to see if the zombie is able to, you know, see a player, and if the zombie sees
a player, then we can directly chase after that player and the head is equal to our zombie dot head.
Now all we can go ahead and do is in the main section.
We're going to create a while loop.
And this while loop is going to execute all of the functionality for our AI.
So the while loop is going to continuously check for any players that are nearby to our zombie.
And then if we can see those players, we're going to chase directly after them.
Otherwise we'll pathfind towards those players.
And if we cannot find any players close by to us, then instead we're going to have the zombie wander
around the map.
So I only want this while loop to execute while our zombie is alive.
So let's go ahead and create a variable.
I'll call it alive and set it equal to true by default.
And we're going to use this as the condition for our while loop.
That way we're going to only execute this code when the zombie is alive.
And then we can have a function listen to when our zombie dies, and then update the value inside of
this variable to false to stop the while loop from executing.
Now in order to see what players are closest to our zombie, we could go ahead and create a function
and I'll call this function get nearest player.
And we need to loop through all of the players in the game and make sure that these players are alive.
They're not on the dead team, and that they are close enough to our zombie for our zombie to be able
to pathfind to them.
So we would loop through every single player and players get players.
And what we want to do is we want to first make sure that this player is on the alive team.
So if this player's team is equal to the dead team, then we don't want to pathfind towards this player.
We're going to ignore them and move on to the next player in this loop.
So let's go ahead and grab the team service.
And then we can check if the player team is equal to the team of dead.
If they're equal to the team of dead, or this player does not have a character, or let's say this
player has a character, but the health of their humanoid is less than or equal to zero, then we're
just going to continue to the next iteration of the loop.
Otherwise, what we can go ahead and do is we can go ahead and calculate the distance between our zombie
and this player.
So we're going to set distance equal to my root part dot position.
Subtract this by the humanoid root part of our player.
So we'll create another variable I'll call this new target.
And we'll set that equal to player dot character dot humanoid root part.
And we're going to subtract by the position of our new target.
And then we can go ahead and get the magnitude between these two positions.
Now, if this distance is less than or equal to our properties dot targeting distance, then we can
go ahead and say, yep, this player is close enough to be our target.
So we can go ahead and create a variable up here I'm going to call it closest target.
Set it equal to nil for now.
And then we would set closest target equal to this new target.
And then we would return closest target at the end of our for loop.
Now there is a little bit of a problem with this situation.
And that's what if there are multiple players within range of our zombie.
Well whatever player last is within range is going to be set as the closest target and they may not
actually be the closest target.
So another thing we need to compare is the last distance of the player compared to another player.
So what I'm going to do is I'm going to create another variable and I'm going to call this last distance.
And by default I'm just going to set it to a very high value like 10,000.
So that way we can compare the first player in the iteration of this loop to this last distance.
So if the distance is less than the targeting distance.
And this distance is also less than the last distance.
Then we can go ahead and set closest target to this new target.
And then we can also update last distance equal to this new distance.
So now we are guaranteed to only grab the player that is closest to our zombie.
Another important thing to note is that I told you earlier that I would like to update the targeting
distance of the zombie.
If the zombie gets shot and is alerted, or the zombie is in the alerted state to go ahead and target
players that are farther away.
So this means we cannot directly use the value stored in the targeting distance property in our module
script.
So instead, what I'm going to do is I'm going to create another variable.
I'm going to call this targeting distance.
And by default we're going to set it to properties dot targeting distance.
But in any case when our zombie gets alerted while the zombie is wandering, we want to update this
value to be equal to the alert targeting distance.
And then we're going to use this variable instead to compare the distance between our zombie and the
player.
Now back inside of our while loop, we could go ahead and get this target from our get nearest player
function.
And here we would check if this function actually returned to us a target.
If it did, then we can go ahead and pathfind towards this target.
Otherwise, if we didn't get a target then this is where we would wander around the map and we could
just print something like wander as a placeholder for now.
Otherwise, if we did get a target, then we want to go ahead and update the walk speed of our humanoid
to be equal to the chase speed, and then we can start generating a path for our zombie to start moving
towards our player, so we can create another function for doing that.
We'll call this create Path to.
And here we will pass the target that we need to generate a path towards.
So inside of this function we can go ahead and get a new path from our pathfinding service.
And we're going to create path and pass the agent parameters that is inside of our properties.
And then we can go ahead and use this path to compute a sync.
The starting point is going to be my root path dot position.
And the ending point is going to be the position of our target.
And then we can go ahead and get all of the waypoints inside of this path.
But the first thing we also want to check is whether or not this path was successfully generated.
So if path dot status is not equal to the enum dot path status of success, then we need to get out
of this function and instead have our zombie wander around the map so we could return path dot status.
And since this function will return to us the status of our path, whether or not it was successful
or unsuccessful, and what we could do down here is call our create path to function.
And actually let me rename this.
I'll do Ctrl shift R, I'll call it compute path to a target.
So we're computing our path to our target that we got from our git nearest player function.
And then we can go ahead and store inside of this success variable whether or not we successfully computed
a path or if we didn't, if we did not successfully compute a path.
So if let's say success is not equal to the enum dot path status of success, then we would also just
wander here as well.
And we could print something like wandering around the map.
And when we are wandering, we would have to set the walk speed of our humanoid back to the default
wandering speed.
Otherwise when we see another target, then we'll update it back to the chasing speed.
How we're back in our compute pass to function.
If we were successful in generating a path, then we want to go ahead and start looping through all
of the waypoints inside of our waypoints table.
And this is where we would use the move two function to start moving our humanoid to each one of these
waypoints.
So we do waypoint dot position.
And then if the action for this waypoint is equal to the enum dot path waypoint action dot jump, then
we'll force our humanoid to jump, just like we did in that previous lecture where I talked about the
pathfinding service.
And then we'll make sure to wait for the My Humanoid Dot move to finished event to fire, and then we
can move on to the next waypoint.
However, we don't want to keep pathfinding if we can see the player.
If the zombie is able to see the player instead of pathfinding, we can just have the zombie repeatedly
run towards our player until we can't see the player anymore, or our zombie dies or the player dies.
So that means we want to have the ability to have a function check to see if we are able to see this
target we're chasing after.
So we can have another function called check visibility.
And we will pass the target to this function.
And it could return to us a boolean of whether or not we can see this player.
And then we could call this function within an if statement and check to see if we can see the target.
And if we can see the target.
Here we could have something like a repeat loop, where we would repeatedly move our zombie towards
the position of our player until either the player died or we died, or we can't see them anymore,
or stuff like that.
So inside of our check visibility function, what we need to do is we need to go ahead and create a
raycast from the head of our zombie to look towards the direction of where our target is, and then
we're going to check if our raycast hit any of the limbs of our player, and if it did, that means
we can see the player.
So let's go ahead and create a new set of raycast parameters.
And we're going to set the filter to send out instances to a table that contains our zombie, because
we don't want the ray to hit our zombie.
We also don't want our ray to hit any invisible parts within the workspace.
So we're going to also include the filtering folder.
And then lastly, we also don't want this zombies raycast to hit other zombies that may be on the map.
So we're going to be parenting all of these zombies in our game to the zombies folder, which means
we can go ahead and pass that folder to this table, and then we're going to ignore any zombies in our
game.
Then we can go ahead and update the filter type equal to the enum dot raycast filter type of exclude.
And then let's go ahead and calculate the direction which is going to be the target position subtracted
by the origin point, which is going to be our heads position.
And then get the unit of that vector.
And then we can go ahead and raycast from our head.
In this direction.
And then we want to multiply this direction by our targeting distance.
So the length of our vector is going to be the same of how far the zombie can detect players.
And then we're also going to pass our raycast parameters.
Then we can go ahead and get the result from this raycast.
And we can check whether or not we got a result.
So if we did get a result and we want to make sure that this result belongs to a player's character
model.
So if the result instance is the descendant.
Of.
And then we're going to pass target dot parent.
Then that means we can see a player.
Because if this instance that we hit is like the limb of the player, let's say it's the leg, then
it's going to definitely be a descendant of the target parent, which would be the player's character.
However, we also need to include the property that we talked about earlier, which was our max visibility
height.
If you remember, if the player is high up in the sky, let's say they're standing on something.
What we're going to technically say the zombie cannot see them because they're too high up.
So this means we need to check the Y position of our player compared to the Y position of our zombie.
So if the target.
That position on the y axis, subtracted by the position of my zombie on the y axis.
And then we want to get the absolute value of this.
So we'll wrap it in math.abs.
If this is greater than our properties dot max visibility height, then that tells us the player is
too far up above the ground or above our zombie to be considered visible, so we're just going to return
false.
Otherwise we can go ahead and return true.
Now if we didn't get a result from our raycast, or the result we got was not a descendant of a player's
character or our target, then we're just going to go ahead and return false.
So now what we can go ahead and do is if we check the visibility of our target and we can actually see
them, then we can just repeatedly have our humanoid move to the position of our target.
And I'm going to offset this position by the target.
See frame dot look vector I'm going to multiply this by two studs to make the length two studs.
Why am I doing this?
Well, this allows our zombie to basically kind of predict where our player is going to move.
You can imagine this position that we're passing right here to be two studs in front of the player.
So this means the zombie is basically going to be trying to move ahead or towards the front of the player,
which will make it look like the zombie is estimating where we're going to move, which looks kind of
cool.
And we can repeatedly do this and we'll have a yield statement in here for like 0.05 seconds.
So we're going to constantly move our humanoid towards our player, and we're going to keep doing that
until either we are no longer able to see the player.
So we can call the check visibility function again and pass our target.
And if we are not able to see the player.
Or let's say we are no longer alive, or let's say our target parent.
We get their humanoid and their health is less than or equal to zero.
So our player is dead.
Then we're going to end and break out of this repeat loop.
And then once we're back in the for loop, since we can no longer see the player or we're not alive
or the player is dead, then we want to completely break out of this for loop and go back into our main
thread and try to find a new nearest player.
So we'll just put a break statement here, and then I'm just going to print out something like.
I am dead or can't see the player or the player is dead.
Otherwise, there's a couple other conditions we want to check in this for loop.
For example, we want to check the distance the target we're running after is away from our humanoid.
So let's say the player is able to run and get far enough away from our zombie where they've gone beyond
the targeting distance.
Well, we need to check for that.
So if my route part dot position, subtract it by the target's position.
If the magnitude of that is greater than the targeting distance, then we need to break out of this
for loop, stop pathfinding and find a new closest player because they have escaped our targeting distance.
Another important thing we need to check is going to be the property of our recalculate path after distance.
So if our zombie has been pathfinding for more than 20 studs and we still haven't seen the player,
then the zombie needs to be like, okay, let me go ahead and see if there's a new closest player that
I can pathfind towards, because I haven't seen this target that I'm trying to go after.
So what we need to do is we need to check if the position of our route part, and we subtract this by
the position of our first waypoint.
So we'll do waypoints, get the first waypoint and then get that position.
If the magnitude between those two positions is greater than our properties dot recalculate path after
distance, then we're also going to break out of this for loop because here this means we've been pathfinding
for too long and didn't see the player.
And then remember here, this is where the player is too far away from our zombie.
And then once we're out of this for loop, we still want to return what the status of our path was.
So we'll make sure to return path dot status.
And that way, if we were successfully generating a path, we can store that inside of our success variable.
And that way we do not start having the zombie wandering around the map, and instead our zombie is
going to go and try to find a new target.
And if we do find that target, then we're going to chase after that particular target.
Now, another important thing we need to do in this main section is we need to apply all of those humanoid
stats that we defined within our properties module script, and that's going to be very easy to do.
We can just go ahead and loop through every single property and its value inside of our properties,
dot humanoid stats.
And then on our humanoid we can go ahead and set that property.
To be equal to that value.
Okay, let's go ahead and test out what we've gotten written out so far.
And I'm also going to go ahead and generate a bunch of parts on the workspace so we can see the path
that is generated by our compute path two function.
So what I'm going to do is I'm going to loop through every single waypoint in waypoints.
And we're going to create a new part.
And we can go ahead and set the shape equal to ball.
So enum dot part type dot ball.
We'll set the material equal to the enum dot material of neon.
We'll set the part's color equal to.
Actually let's go ahead and generate a random color.
So I'll create a variable called color that's going to be equal to color three from RGB.
Or actually we could do dot new.
And then we're just going to pass math dot random and then math dot random and then math dot random.
And then we can go ahead and set the color of our part equal to this new color.
So that way every single path that the AI generates is going to be a different color.
And then we can set the size equal to a new vector three dot new.
And we'll just do like half a stud in size.
We'll set the position of the part equal to this waypoints position.
We'll set anchored equal to true.
We'll set can collide equal to false.
And then we'll make sure to parent this into the workspace.
So that way we are able to visualize the path of our zombie generates.
So now we can go ahead and keep our zombie over here.
And actually one last very important thing we need to do is we actually need to ensure that we are yielding
in this while loop.
So we need to make sure to put a yield statement down here.
And we could wait for like a 10th of a second.
So we'll do 0.1 seconds.
That way we don't accidentally crash our game because we don't want to have a while loop that's repeatedly
executing without a yield statement.
So we make sure we'll yield and then we can go ahead and play test our game.
As you can see, it already is being printed out in the console that our zombie is quote unquote wandering
because there's no player nearby.
But now that I'm on the map, if I get close to my zombie.
He isn't chasing me.
He's still wandering around the map for some reason.
So he's not detecting that I am nearby.
And let's go ahead and see why that may be the case.
So we should be getting a target here from our get nearest player function.
If we actually go to the get nearest player function.
The reason is we forgot to put a Not statement here.
So we need to make sure that this player does not have a character and we're skipping them.
Not that they do have a character.
So we'll make sure to put that not statement here.
And now we can go ahead and put this the game again.
And it says our zombie is wandering.
And then if we spawn into the map.
As you can see, it stopped wandering and now you can see the this point was generated.
Oh, there's another path.
And our zombie is now starting to walk towards us.
As you can see, our zombie isn't walking along that path.
It generated because it can see our player, and instead our repeat loop is forcing our zombie to continually
chase after our player.
But once I break the line of sight with my zombie, my zombie should generate a new path to try and
get after me.
So, for example, if I run all the way down here and get behind this pillar.
As you can see, the zombie now has to pathfind to get towards me.
And now that he sees me again, he's coming towards me once more and he's chasing after me.
If I go around this corner once again.
As you can see, the zombie has lost sight of me.
And we are running into an issue where our zombie is stuck.
Okay, now he's moving.
We're going to address that problem we just saw in a little bit.
But you may be seeing that the zombie is being how could I say it very slow when pathfinding.
So again he's frozen again.
And then we're going to be waiting.
Okay.
You can see as he moves to each one of these points, he's kind of like stopping, starting, stopping,
starting.
That's because I am recording and hosting this server on my computer at the same time.
So the server is being a little bit slow with, uh, my I zombie.
If I wasn't recording then the pathfinding would look very smooth.
That's just an artifact of me recording and doing this at the same time.
But let's go ahead and address that problem where our zombie seems to freeze in place before he starts
pathfinding again.
And the reason this is occurring is because if we go.
To.
I believe when we're waiting for this event right here, sometimes our humanoid may not be able to move
to a particular waypoint for some reason, and this event will be automatically fired after a certain
period of time if our zombie doesn't move to a position.
But we do not want to wait that long for our zombie to be stuck there.
Sitting there.
So what we want to do instead is we want to be able to basically have a time out here where we're either
going to wait for this event to fire, or we're going to wait for a specific amount of time to pass.
Now, unfortunately, Roblox does not have an inbuilt feature to this wait function where we can supply
a number to act as a timeout, which means we're going to have to implement it ourselves.
And actually, it's not too complicated to implement a function to either wait for an event or wait
for a timeout to happen.
And we're going to be using coroutines to create this function.
So what I'm going to do is at the top, I'm going to create a new function.
And I'm going to call this wait with timeout.
And what we're going to pass to this function is an event which would be an RPC script signal and a
number which would be our timeout.
Now what we're going to do in this function is we're going to grab the current executing thread inside
of this function.
And then we're going to yield that thread.
And we're only going to resume the thread once.
Either this event fires or this timeout is reached.
Now to get the current thread that is executing within this function we can use the coroutine dot running
function.
And it says returns the running coroutine.
So we can get the current executing thread and store it in a variable like that.
So this is the current executing thread that is going through this function.
And then what we want to do is we want to connect a function to this event.
And I'm going to type annotate this as an RPC script signal.
You don't need to worry as always about type annotation, I'm just doing it for the sake of this lecture.
But that way we can actually see the different functions for our event.
And we're going to connect a function once to this event, pass our function here.
And then we're also going to do something at the same time, which is we're going to delay a function
from running.
And we're going to delay it equal to the timeout that is passed to our function.
And now this means either this function right here is going to execute first, or this function right
here is going to execute first.
Whichever one executes first, their purpose is going to be resuming this thread, allowing the main
thread of execution to come back to this function and return out of it.
And we should be able to return a true or false boolean to let us know whether or not we timed out or
the event fired.
Now, in order to yield the thread inside of this function, we can return.
And then we're going to call the coroutine dot yield function to suspend the execution of our thread.
So when the main thread of execution goes into this function, it's going to set all of this up.
And then it's going to hit this statement right here.
And it's going to pause or freeze inside of our function.
And it's only going to come back to this point and return out of our function once we actually resume
the thread.
And we can do that either within our task dot delay function or the function connected to our event.
So, for example, what we need to do is we actually need to store the connection.
To this event, and this is how we're going to keep track of whether or not this function executed or
this function executed.
Let's say we reach the timeout before this event ever executes.
Well, what we want to go ahead and do is we want to disconnect the connection, and then we want to
set connection equal to nil.
And this is how we're going to keep track if one of these functions executed first.
Because let's say this function executed and then set connection equal to nil.
And then this function executed well we can check in this function if connection is equal to nil.
If it is that means this function up here already executed.
And we can go ahead and return and do nothing.
And we could go ahead and do the same thing here as well.
If this function executed before this function does, we can check if connection is equal to nil.
And if it is then we're also going to return.
Otherwise inside of this one we can go ahead and set connection equal to nil.
I don't need to disconnect the connection right here because we are using the once function, which
will automatically disconnect it for us.
But after either this function executes first or this function executes first, we can go ahead and
use the task dot spawn function to resume our thread, because we can pass a function or a thread here,
and this is where we're going to pass our thread to resume it.
And then other additional information we're going to pass here is going to be a boolean of true or false,
since I want this to return a boolean.
If it timed out, then if this function right here executed, then that means yes, we did time out
so we can return true.
Otherwise we can copy this and resume the thread here and pass false instead.
Meaning no, we did not time out the event fired and then since this event might pass additional information,
I'll make sure to also pass that to my task dot spawn function.
But now we have created a simple function that's either going to wait for our event to fire, or it's
going to wait for our timeout to reach, and then it's going to return to us a true or false buoyant
to let us know whether or not we timed out.
Now we can go down to our Compute path two function, and instead of using the wait function to wait
for the move to finish event, we can instead call our wait with timeout function.
Pass my humanoid dot move to finished and then we could pass a timeout.
We wish to wait and we could do something like maybe three seconds, and then we can store in a variable
whether or not we timed out.
So I'll just call this timed out.
And then we can go ahead and check to see if we timed out.
If we timed out, then that means our humanoid did not make it to our waypoint within this time threshold.
And that could indicate that our humanoid is stuck.
So I'll print out a warning and say zombie timed out and we could ask something like is it stuck?
And what I'm going to do is just in case.
Maybe my zombie is, like, stuck in a little divot or something.
I'll force my humanoid to jump.
And then we're going to compute a path to our target and try again.
And we're going to break.
Out of our for loop.
And actually what we could do instead is, since we're calling the compute path two function within
the compute path two function, which is a recursive function call, we can go ahead and store the result
from our compute path two function.
And instead of breaking here, we'll just return that result when we break out of this function right
here.
So we're only going to recompute the path just in case our zombie gets stuck.
And it has to generate a new path to a new closest player.
So now we can go ahead and playtest our game again.
And as you can see, it says our zombie is quote unquote wandering.
And then once we spawn into the map, we are now within the targeting range for our zombie.
As you can see, our zombie is trying to run directly towards us because the raycast is hitting our
player through those bars.
But as soon as I break the line of sight of that raycast, as you can see, our zombie generated a new
path.
But since our zombie can see me, he's chasing after me.
And then if I break and lose sight of him.
As you can see now he is pathfinding to get towards us and he's continuing to generate a new path.
So he's continuing to generate his path to generating a path.
So as you can see, since my zombie is going along this path more than 20 studs, he is recomputing
the path to make sure that there isn't a new closest player nearby, and it's of course, grabbing us
instead because we're the only player in the game.
But once he path finds for more than 20 studs, he's going to recompute the path again.
As you can see, he computes it again and then he should.
There we go.
He computed another path, and that's because he's reaching that threshold where the zombie is like,
okay, I've been pathfinding for more than 20 studs and I don't see the player.
Of course, you can go ahead and modify this value to be larger.
Like maybe you want to set it to 30 or 40 studs, but I found a value of 20 studs to be decent enough
to where our zombie is going to pathfind towards us, and then once he path finds for too long, he
should recompute a new path.
As you can see, he just recomputed a new path again.
He recomputed another one and now he sees us and he's chasing after us and that's pretty cool.
Okay, so let's go through a quick recap of the code we've just written, just to make sure you understand
everything that is going on.
So we have a while loop at the bottom of our code that's going to constantly check for the closest player
to our zombie.
So this get nearest player function goes through all the players in our game checks the distances between
all those players, and then it returns to us the closest target to us.
If this function returns a target, then we're going to go ahead and compute a path towards that target
and start chasing after them.
Otherwise, if we did not find a target, we're going to wander around the map instead.
Now, inside of our compute path two function, what we're doing is we are generating a new path using
the pathfinding service.
And then we're checking to make sure that we were successful in generating that path to this particular
player.
If we were not successful, then that means our zombie is going to wander around the map.
Otherwise we start moving our zombie along the different waypoint positions.
If our zombie is able to see our player, then instead our zombie is going to continually run after
our player until the zombie is no longer able to see them, or our zombie dies.
Or.
The health of our humanoid.
The player we're chasing after hits zero.
We're also checking if the player gets out of range of the targeting distance for our zombie, and we're
also checking to see if we've been pathfinding for more than 20 studs.
And if we are, we're going to recompute a new path by breaking out of this for loop.
Now, if for some reason the move two finished event for our humanoid does not fire within our timeout,
then that could possibly mean that our zombie is stuck.
So what we're going to do is we're going to force our zombie to jump, and then we're going to recompute
a new path to our target to see if it is possible to get back on track with our target.
And then we'll return that result here out of our function, which means it'll go back to the while
loop and store that in this variable.
And then we can check whether or not we were successful.
If we weren't, we'll wander around the map.
Otherwise we'll go through the next iteration of this while loop, find a new target, and start chasing
after that player, starting the entire loop all over again.
So this is just a constant loop of our zombie trying to find any players that are close to us.
Now of course, another thing I want to go ahead and add is I want to update the alive variable if our
zombie dies.
And that's very easy to do.
If my humanoid and the died event fires, then we can connect a function set alive equal to false.
And then we could wait for a small period of time, like five seconds, and then we'll simply just destroy
our zombie off the map because our zombie is dead.
And actually, another very important thing we need to do for our zombie is that since the server is
controlling the pathfinding for our zombie, we also want the server to have network ownership over
our zombie, and we don't want players to have a network ownership over our zombies.
So inside of the main section, another thing we can do is we can refer to our root part and we'll set
the network owner equal to nil, which will automatically set the network owner equal to the server.
So now the server has network ownership over our zombie.
No player is ever going to get network ownership back over our zombie unless the server sets network
ownership back to be automatic, and we can go ahead and do that when our zombie dies.
Because when our zombie dies, we don't care about the zombie anymore.
So we can set the network ownership back to automatic for our root part.
Now, another thing I want to actually listen for is I want to listen for when the health changes within
my humanoid.
So we're going to listen to the health changed event.
We're going to connect and get a function.
And this function is going to pass to us the new health that was set on our humanoid.
And we want to see if this health is less than our zombies previous health.
If it is, then that means our zombie got hurt by something which is probably a player, and that means
we want to update our targeting distance variable to be equal to that properties dot alert targeting
distance.
So we can go ahead and keep track of the zombies previous health.
With any variable, we'll call it current health.
And by default we'll set that to properties dot humanoid stats dot health.
And back inside of our event listener, we can go ahead and check to see if the new health that our
humanoid was set to is less than our current health, which means our zombie got damaged.
So if our zombie got damaged, we're going to update the targeting distance to be equal to the alert
targeting distance.
And then we'll also make sure to set current health to be equal to this new health.
Okay, so we've got a lot done for our zombie.
And this lecture is getting a little bit long.
So we're going to continue scripting our AI zombie.
And the next lecture specifically we're going to be adding the different sounds for our zombies.
We're going to be allowing our zombie to wander around when it can't find any nearby players.
We're going to be adding health regeneration as well as a few more things.
So I will see you in the next lecture.